/****************************************************************************
**
** This file is part of the KD Chart library.
**
** SPDX-FileCopyrightText: 2001-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
**
** SPDX-License-Identifier: MIT
**
****************************************************************************/

#ifndef KDCHARTABSTRACTCOORDINATEPLANE_H
#define KDCHARTABSTRACTCOORDINATEPLANE_H

#include <QList>
#include <QObject>

#include "KDChartAbstractArea.h"
#include "KDChartAbstractDiagram.h"
#include "KDChartEnums.h"

namespace KDChart {

class Chart;
class GridAttributes;
class DataDimension;

typedef QList<DataDimension> DataDimensionsList;

/**
 * @brief Base class common for all coordinate planes, CartesianCoordinatePlane, PolarCoordinatePlane, TernaryCoordinatePlane
 */
class KDCHART_EXPORT AbstractCoordinatePlane : public AbstractArea
{
    Q_OBJECT

    KDCHART_DECLARE_PRIVATE_DERIVED_PARENT(AbstractCoordinatePlane, Chart *)

    friend class AbstractGrid;

public:
    enum AxesCalcMode
    {
        Linear,
        Logarithmic
    };

protected:
    explicit AbstractCoordinatePlane(Chart *parent = nullptr);

public:
    ~AbstractCoordinatePlane() override;

    /**
     * Adds a diagram to this coordinate plane.
     * @param diagram The diagram to add.
     *
     * \sa replaceDiagram, takeDiagram
     */
    virtual void addDiagram(AbstractDiagram *diagram);

    /**
     * Replaces the old diagram, or appends the
     * diagram, it there is none yet.
     *
     * @param diagram The diagram to be used instead of the old diagram.
     * This parameter must not be zero, or the method will do nothing.
     *
     * @param oldDiagram The diagram to be removed by the new diagram. This
     * diagram will be deleted automatically. If the parameter is omitted,
     * the very first diagram will be replaced. In case, there was no
     * diagram yet, the new diagram will just be added.
     *
     * \note If you want to re-use the old diagram, call takeDiagram and
     * addDiagram, instead of using replaceDiagram.
     *
     * \sa addDiagram, takeDiagram
     */
    virtual void replaceDiagram(AbstractDiagram *diagram, AbstractDiagram *oldDiagram = nullptr);

    /**
     * Removes the diagram from the plane, without deleting it.
     *
     * The plane no longer owns the diagram, so it is
     * the caller's responsibility to delete the diagram.
     *
     * \sa addDiagram, replaceDiagram
     */
    virtual void takeDiagram(AbstractDiagram *diagram);

    /**
     * @return The first diagram associated with this coordinate plane.
     */
    AbstractDiagram *diagram();

    /**
     * @return The list of diagrams associated with this coordinate plane.
     */
    AbstractDiagramList diagrams();

    /**
     * @return The list of diagrams associated with this coordinate plane.
     */
    ConstAbstractDiagramList diagrams() const;

    /**
     * Distribute the available space among the diagrams and axes.
     */
    virtual void layoutDiagrams() = 0;

    /**
     * Translate the given point in value space coordinates to a position
     * in pixel space.
     * @param diagramPoint The point in value coordinates.
     * @returns The translated point.
     */
    virtual const QPointF translate(const QPointF &diagramPoint) const = 0;

    /**
     * @return Whether zooming with a rubber band using the mouse is enabled.
     */
    bool isRubberBandZoomingEnabled() const;

    /**
     * Enables or disables zooming with a rubber band using the mouse.
     */
    void setRubberBandZoomingEnabled(bool enable);

    /**
     * @return The zoom factor in horizontal direction, that is applied
     * to all coordinate transformations.
     */
    virtual qreal zoomFactorX() const
    {
        return 1.0;
    }

    /**
     * @return The zoom factor in vertical direction, that is applied
     * to all coordinate transformations.
     */
    virtual qreal zoomFactorY() const
    {
        return 1.0;
    }

    /**
     * Sets both zoom factors in one go.
     * \sa setZoomFactorX,setZoomFactorY
     */
    virtual void setZoomFactors(qreal factorX, qreal factorY)
    {
        Q_UNUSED(factorX);
        Q_UNUSED(factorY);
    }

    /**
     * Sets the zoom factor in horizontal direction, that is applied
     * to all coordinate transformations.
     * @param factor The new zoom factor
     */
    virtual void setZoomFactorX(qreal factor)
    {
        Q_UNUSED(factor);
    }

    /**
     * Sets the zoom factor in vertical direction, that is applied
     * to all coordinate transformations.
     * @param factor The new zoom factor
     */
    virtual void setZoomFactorY(qreal factor)
    {
        Q_UNUSED(factor);
    }

    /**
     * @return The center point (in value coordinates) of the
     * coordinate plane, that is used for zoom operations.
     */
    virtual QPointF zoomCenter() const
    {
        return QPointF(0.0, 0.0);
    }

    /**
     * Set the point (in value coordinates) to be used as the
     * center point in zoom operations.
     * @param center The point to use.
     */
    virtual void setZoomCenter(const QPointF &center)
    {
        Q_UNUSED(center);
    }

    /**
     * Set the grid attributes to be used by this coordinate plane.
     * To disable grid painting, for example, your code should like this:
     * \code
     * GridAttributes ga = plane->globalGridAttributes();
     * ga.setGlobalGridVisible( false );
     * plane->setGlobalGridAttributes( ga );
     * \endcode
     * \sa globalGridAttributes
     * \sa CartesianCoordinatePlane::setGridAttributes
     */
    void setGlobalGridAttributes(const GridAttributes &);

    /**
     * @return The grid attributes used by this coordinate plane.
     * \sa setGlobalGridAttributes
     * \sa CartesianCoordinatePlane::gridAttributes
     */
    GridAttributes globalGridAttributes() const;

    /**
     * Returns the dimensions used for drawing the grid lines.
     *
     * Returned data is the result of (cached) grid calculations,
     * so - if you need that information for your own tasks - make sure to
     * call again this function after every data modification that has changed
     * the data range, since grid calculation is based upon the data range,
     * thus the grid start/end might have changed if the data was changed.
     *
     * @note Returned list will contain different numbers of DataDimension,
     * depending on the kind of coordinate plane used.
     * For CartesianCoordinatePlane two DataDimension are returned: the first
     * representing grid lines in X direction (matching the Abscissa axes)
     * and the second indicating vertical grid lines (or Ordinate axes, resp.).
     *
     * @return The dimensions used for drawing the grid lines.
     * @sa DataDimension
     */
    DataDimensionsList gridDimensionsList();

    /**
     * Set another coordinate plane to be used as the reference plane
     * for this one.
     * @param plane The coordinate plane to be used the reference plane
     * for this one.
     * @see referenceCoordinatePlane
     */
    void setReferenceCoordinatePlane(AbstractCoordinatePlane *plane);

    /**
     * There are two ways, in which planes can be caused to interact, in
     * where they are put layouting wise: The first is the reference plane. If
     * such a reference plane is set, on a plane, it will use the same cell in the
     * layout as that one. In addition to this, planes can share an axis. In that case
     * they will be laid out in relation to each other as suggested by the position
     * of the axis. If, for example Plane1 and Plane2 share an axis at position Left,
     * that will result in the layout: Axis Plane1 Plane 2, vertically. If Plane1
     * also happens to be Plane2's reference plane, both planes are drawn over each
     * other. The reference plane concept allows two planes to share the same space
     * even if neither has any axis, and in case there are shared axis, it is used
     * to decided, whether the planes should be painted on top of each other or
     * laid out vertically or horizontally next to each other.
     * @return The reference coordinate plane associated with this one.
     */
    AbstractCoordinatePlane *referenceCoordinatePlane() const;

    /**
     * @return Whether this plane should have spacers in the corners
     * formed by the presence of axes.
     */
    bool isCornerSpacersEnabled() const;

    /**
     * Enables or disables the use of spacers in the plane corners.
     */
    void setCornerSpacersEnabled(bool enable);

    virtual AbstractCoordinatePlane *sharedAxisMasterPlane(QPainter *p = nullptr); // KDChart 3: const method?

    /** pure virtual in QLayoutItem */
    bool isEmpty() const override;
    /** pure virtual in QLayoutItem */
    Qt::Orientations expandingDirections() const override;
    /** pure virtual in QLayoutItem */
    QSize maximumSize() const override;
    /** pure virtual in QLayoutItem */
    QSize minimumSize() const override;
    /** pure virtual in QLayoutItem */
    QSize sizeHint() const override;
    /** pure virtual in QLayoutItem
     *
     * \note Do not call this function directly, unless you know
     * exactly what you are doing.  Geometry management is done
     * by KD Chart's internal layouting measures.
     */
    void setGeometry(const QRect &r) override;
    /** pure virtual in QLayoutItem */
    QRect geometry() const override;

    virtual void mousePressEvent(QMouseEvent *event);
    virtual void mouseDoubleClickEvent(QMouseEvent *event);
    virtual void mouseMoveEvent(QMouseEvent *event);
    virtual void mouseReleaseEvent(QMouseEvent *event);

    /**
     * Called internally by KDChart::Chart
     */
    void setParent(Chart *parent);
    Chart *parent();
    const Chart *parent() const;

    /**
     * Tests, if a point is visible on the coordinate plane.
     *
     * \note Before calling this function the point must have been translated into coordinate plane space.
     */
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(Q_COMPILER_MANGLES_RETURN_TYPE)
    const bool isVisiblePoint(const QPointF &point) const;
#else
    bool isVisiblePoint(const QPointF &point) const;
#endif

public Q_SLOTS:
    /**
     * Calling update() on the plane triggers the global KDChart::Chart::update()
     */
    void update();
    /**
     * Calling relayout() on the plane triggers the global KDChart::Chart::slotRelayout()
     */
    void relayout();
    /**
     * Calling layoutPlanes() on the plane triggers the global KDChart::Chart::slotLayoutPlanes()
     */
    void layoutPlanes();
    /**
     * Used by the chart to clear the cached grid data.
     */
    void setGridNeedsRecalculate();

Q_SIGNALS:
    /** Emitted when this coordinate plane is destroyed. */
    void destroyedCoordinatePlane(AbstractCoordinatePlane *);

    /** Emitted when plane needs to update its drawings. */
    void needUpdate();

    /** Emitted when plane needs to trigger the Chart's layouting. */
    void needRelayout();

    /** Emitted when plane needs to trigger the Chart's layouting of the coord. planes. */
    void needLayoutPlanes();

    /** Emitted upon change of a property of the Coordinate Plane or any of its components. */
    void propertiesChanged();

    void boundariesChanged();

    /** Emitted after the geometry of the Coordinate Plane has been changed.
     *  and control has returned to the event loop.
     *
     * Parameters are the old geometry, the new geometry.
     */
    void geometryChanged(QRect, QRect);

private:
Q_SIGNALS:
    // Emitted from inside the setGeometry()
    // This is connected via QueuedConnection to the geometryChanged() Signal
    // that users can connect to safely then.
    void internal_geometryChanged(QRect, QRect);
    /** Emitted upon change of the view coordinate system */
    void viewportCoordinateSystemChanged();

protected:
    virtual DataDimensionsList getDataDimensionsList() const = 0;

    // KDCHART_DECLARE_PRIVATE_DERIVED( AbstractCoordinatePlane )
};

/**
 * \brief Helper class for one dimension of data, e.g. for the rows in a data model,
 * or for the labels of an axis, or for the vertical lines in a grid.
 *
 * isCalculated specifies whether this dimension's values are calculated or counted.
 * (counted == "Item 1", "Item 2", "Item 3" ...)
 *
 * sequence is the GranularitySequence, as specified at for the respective
 * coordinate plane.
 *
 * Step width is an optional parameter, to be omitted (or set to Zero, resp.)
 * if the step width is unknown.
 *
 * The default c'tor just gets you counted values from 1..10, using step width 1,
 * used by the CartesianGrid, when showing an empty plane without any diagrams.
 */
class DataDimension
{
public:
    DataDimension()
    {
    }
    DataDimension(qreal start_,
                  qreal end_,
                  bool isCalculated_,
                  AbstractCoordinatePlane::AxesCalcMode calcMode_,
                  KDChartEnums::GranularitySequence sequence_,
                  qreal stepWidth_ = 0.0,
                  qreal subStepWidth_ = 0.0)
        : start(start_)
        , end(end_)
        , isCalculated(isCalculated_)
        , calcMode(calcMode_)
        , sequence(sequence_)
        , stepWidth(stepWidth_)
        , subStepWidth(subStepWidth_)
    {
    }
    /**
     * Returns the size of the distance,
     * equivalent to the width() (or height(), resp.) of a QRectF.
     *
     * Note that this value can be negative, e.g. indicating axis labels
     * going in reversed direction.
     */
    qreal distance() const
    {
        return end - start;
    }

    bool operator==(const DataDimension &r) const
    {
        return (start == r.start) && (end == r.end) && (sequence == r.sequence) && (isCalculated == r.isCalculated) && (calcMode == r.calcMode) && (stepWidth == r.stepWidth) && (subStepWidth == r.subStepWidth);
    }

    bool operator!=(const DataDimension &other) const
    {
        return !operator==(other);
    }

    qreal start = 1.0;
    qreal end = 10.0;
    bool isCalculated = false;
    AbstractCoordinatePlane::AxesCalcMode calcMode = AbstractCoordinatePlane::Linear;
    KDChartEnums::GranularitySequence sequence = KDChartEnums::GranularitySequence_10_20;
    qreal stepWidth = 1.0;
    qreal subStepWidth = 0.0;
};

#if !defined(QT_NO_DEBUG_STREAM)
QDebug operator<<(QDebug stream, const DataDimension &r);
#endif
}
#endif
